import axios from "axios";
import { getCacheRecord, getFileSize, updateCacheRecord } from "../../helper";
import Signer from "./vod/sign";
import fs from "node:fs";
import { readFileCrc32 } from "./helper";

export class Uploader {
  static VIDEO_MAX_SIZE = 5 * 1024 * 1024; // 5242880
  static VIDEO_UPLOAD_URL = 'https://vod.bytedanceapi.com/';
  static IMAGE_UPLOAD_URL = 'https://imagex.bytedanceapi.com';
  static UPLOAD_SERVICE_ID = 'jm8ajry58r';
  static CONCURRENT_UPLOADS = 3;
  static CACHE_FILE_PATH = 'douyin_video.json';
  static CACHE_EXPIRATION_TIME = 60 * 60 * 1000;

  constructor(options){
    this.options = options
  }

  async uploadVideo(filePath, cache = true){
    if (cache) {
      const cachedFile = this.getResponseByCache(filePath, Uploader.CACHE_FILE_PATH);
  
      if (cachedFile) {
        return cachedFile;
      }
    }

    const fileSize = await getFileSize(filePath);

    const [ uploadUrl, uploadAuthorization, videoId, sessionKey ] = await this.getApplyUploadInner(fileSize);

    if (fileSize <= Uploader.VIDEO_MAX_SIZE){
      await this.uploadLittleContent(uploadUrl, filePath, uploadAuthorization)
    } else {
      await this.uploadBigContent(uploadUrl, filePath, uploadAuthorization)
    }

    await this.getCommitUploadInner(sessionKey);

    if (cache) {
      updateCacheRecord(filePath, videoId, Uploader.CACHE_FILE_PATH);
    }

    return videoId;
  }

  async uploadLittleContent(uploadUrl, filePath, uploadAuthorization){
    const fileContent = await fs.promises.readFile(filePath);
    const fileSize = await getFileSize(filePath);    
    const crc32 = readFileCrc32(fileContent);

    const headers = {
      'Authorization': uploadAuthorization,
      'Content-Length': fileSize,
      'Content-CRC32': crc32,
      'Referer': 'https://creator.douyin.com/',
      'User-Agent': this.options.UserAgent,
      'X-Logical-Part-Mode': 'logical_part',
      'X-Storage-U': encodeURIComponent(this.options.ThirdId),
    };

    await axios({
      url: uploadUrl,
      method: 'POST',
      headers,
      data: fileContent,
      transformRequest: [(data) => data]
    });
  }

  async uploadBigContent(uploadUrl, filePath, uploadAuthorization){
    const fileSize = await getFileSize(filePath);
    const lastChunkSize = fileSize % Uploader.VIDEO_MAX_SIZE;

    // 检查最后一块的大小
    let actualChunkCount = Math.ceil(fileSize / Uploader.VIDEO_MAX_SIZE);
    if (lastChunkSize > 0 && lastChunkSize < Uploader.VIDEO_MAX_SIZE) {
        --actualChunkCount;  // 减少一次分片
    }

    // 第一步：获取 uploadID
    const initResponse = await axios({
      url: uploadUrl,
      params: {
        uploadmode: 'part',
        phase: 'init',
      },
      method: 'POST',
      headers: {
        Authorization: uploadAuthorization,
        Referer: 'https://creator.douyin.com/',
        'Sec-Gpc': 1,
        'User-Agent': this.options.UserAgent,
        'X-Logical-Part-Mode': 'logical_part',
        'X-Storage-Mode': 'gateway',
        'X-Storage-U': encodeURIComponent(this.options.ThirdId),
      }
    });

    const uploadID = initResponse.data.data.uploadid;

    // 第二步：分段上传文件
    let partInfo = [];
    const fileHandle = await fs.promises.open(filePath, 'r');

    try {
      // 创建分片上传任务
      const uploadTasks = Array.from({ length: actualChunkCount }, (_, i) => {
        return async () => {
          const start = i * Uploader.VIDEO_MAX_SIZE;
          const end = Math.min(start + Uploader.VIDEO_MAX_SIZE, fileSize);

          let chunkSize = end - start;

          // 如果是最后一块，并且还有剩余内容需要合并
          if (i === actualChunkCount - 1 && actualChunkCount !== fileSize / Uploader.VIDEO_MAX_SIZE) {
            chunkSize = fileSize - start;
          }
          
          const buffer = Buffer.alloc(chunkSize);
          await fileHandle.read(buffer, 0, chunkSize, start);
          const crc32 = readFileCrc32(buffer);
          
          await axios({
            url: uploadUrl,
            params: {
              phase: 'transfer',
              part_number: i + 1,
              part_offset: start,
              uploadid: uploadID
            },
            method: 'POST',
            headers: {
              'Authorization': uploadAuthorization,
              'Content-Type': 'application/octet-stream',
              'Content-Length': chunkSize,
              'Content-CRC32': crc32,
              'X-Storage-U': encodeURIComponent(this.options.ThirdId),
              'Referer': 'https://creator.douyin.com/',
              'User-Agent': this.options.UserAgent,
              'X-Logical-Part-Mode': 'logical_part',
              'X-Storage-Mode': 'gateway'
            },
            data: buffer,
            transformRequest: [(data) => data]
          });

          return {
            partNumber: i + 1,
            crc32
          };
        };
      });

      // 并发执行上传任务，限制最大并发数
      const results = [];
      for (let i = 0; i < uploadTasks.length; i += Uploader.CONCURRENT_UPLOADS) {
        const batch = uploadTasks.slice(i, i + Uploader.CONCURRENT_UPLOADS);
        const batchResults = await Promise.all(batch.map(task => task()));
        results.push(...batchResults);
      }

      // 按分片序号排序并生成 partInfo
      partInfo = results
        .sort((a, b) => a.partNumber - b.partNumber)
        .map(({ partNumber, crc32 }) => `${partNumber}:${crc32}`);
    } finally {
      await fileHandle.close();
    }

    // 第三步：完成分片上传
    await axios({
      url: uploadUrl,
      params: {
        uploadmode: 'part',
        phase: 'finish',
        uploadid: uploadID
      },
      method: 'POST',
      headers: {
        'Authorization': uploadAuthorization,
        'Content-Type': 'text/plain;charset=UTF-8',
        Referer: 'https://creator.douyin.com/',
        'User-Agent': this.options.UserAgent,
        'X-Logical-Part-Mode': 'logical_part',
        'X-Storage-Mode': 'gateway'
      },
      data: partInfo.join(',')
    });
  }

  async getApplyUploadInner(fileSize){
    const params = {
      "Action": "ApplyUploadInner",
      "Version": "2020-11-19",
      "SpaceName": "aweme",
      "FileType": "video",
      "IsInner": 1,
      "FileSize": fileSize,
      "app_id": 2906,
      "user_id": this.options.ThirdId,
      "s": Math.random().toString(36).substr(2)
    }

    const request = {
      method: 'GET',
      params,
      pathname: '/',
      region: "cn-north-1",
      timeout: 30000,
      url: `${Uploader.VIDEO_UPLOAD_URL}?${new URLSearchParams(params).toString()}`,
    }

    const signer = new Signer(request, 'vod');
    signer.addAuthorization({ 
      accessKeyId: this.options.AccessKeyID,
      secretKey: this.options.SecretAccessKey,
      sessionToken: this.options.SessionToken
    })


    const response = await axios({
      url: Uploader.VIDEO_UPLOAD_URL,
      method: request.method,
      params: request.params,
      headers: {
        ...request.headers,
        'User-Agent': this.options.UserAgent,
        Referer: "https://creator.douyin.com/"
      },
    })

    if(response.data.ResponseMetadata.Error){
      throw new Error(response.data.ResponseMetadata.Error.Message)
    }

    const result = response.data.Result

    const uploadHost = result.InnerUploadAddress.UploadNodes[0].UploadHost;
    const uploadUrl = `https://${uploadHost}/upload/v1/${result.InnerUploadAddress.UploadNodes[0].StoreInfos[0].StoreUri}`;
    const uploadAuthorization = result.InnerUploadAddress.UploadNodes[0].StoreInfos[0].Auth;
    const videoId = result.InnerUploadAddress.UploadNodes[0].Vid;
    const sessionKey = result.InnerUploadAddress.UploadNodes[0].SessionKey;

    return [uploadUrl, uploadAuthorization, videoId, sessionKey];
  }

  async getCommitUploadInner(sessionKey){
    const params = {
      'Action': 'CommitUploadInner',
      'SpaceName': 'aweme',
      'Version': '2020-11-19'
    };

    const requestBody = {
      'SessionKey': sessionKey,
      'Functions': [
        { 'name': 'GetMeta' },
        { 'name': 'Snapshot', 'input': { 'SnapshotTime': 0 } }
      ]
    };

    const request = {
      body: requestBody,
      method: 'POST',
      params,
      pathname: '/',
      region: 'cn-north-1',
      timeout: 30000,
      url: `${Uploader.VIDEO_UPLOAD_URL}?${new URLSearchParams(params).toString()}`
    };

    const signer = new Signer(request, 'vod');
    signer.addAuthorization({
      accessKeyId: this.options.AccessKeyID,
      secretKey: this.options.SecretAccessKey,
      sessionToken: this.options.SessionToken
    });

    const response = await axios({
      url: request.url,
      method: request.method,
      headers: {
        ...request.headers,
        'User-Agent': this.options.UserAgent,
        Referer: "https://creator.douyin.com/"
      },
      data: requestBody
    });

    if(response.data.ResponseMetadata.Error){
      throw new Error(response.data.ResponseMetadata.Error.Message)
    }

    return response.data.Result;
  }

  getResponseByCache(filePath, cacheFilePath) {
    const records = getCacheRecord(cacheFilePath);
    
    if (records[filePath]) {
      const { content, timestamp } = records[filePath];
      const currentTime = Date.now();

      if (currentTime - timestamp <= Uploader.CACHE_EXPIRATION_TIME) {
        return content;
      }
    }
  
    return null;
  }
}